#include <opencv2/core/types.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/opencv.hpp>
#include <stdio.h>
#include <stdlib.h>

class Point
{
public:
    int x;
    int y;
};

#if __linux__
enum
{
    KEY_LEFT = 81,
    KEY_RIGHT = 83,
    KEY_TOP = 82,
    KEY_BOTTOM = 84,
    KEY_EXIT = 27
};
#elif __APPLE__
enum
{
    KEY_LEFT = 2,
    KEY_RIGHT = 3,
    KEY_TOP = 0,
    KEY_BOTTOM = 1,
    KEY_EXIT = 27
};
#endif

// Divide the whole image(canvas) into blocks
// block is the basic plot unit
class Block
{
public:
    int width;
    int height;
public:
    Block(int _height, int _width) : height(_height), width(_width) {}
};

// A randomly generated block
class Food
{
public:
    Point pos;
    cv::Scalar color;
    bool exist;
};

// User use arrow keys for snake movement
// Esc key corresponds to EXIT, means exit the game
enum Direction
{
    NONE,
    LEFT,
    RIGHT,
    TOP,
    BOTTOM,
    EXIT
};

class Snake
{
public:
    Snake() {}
    Snake(const Point& pos, cv::Scalar color);

public:
    std::vector<Point> points;
    cv::Scalar color;
};

static void generate_food();

//----------
// 全局变量
Direction direction;
const int width = 640;
const int height = 480;
Point center;
Block block(40, 40);
Snake snake;
cv::Mat image;
Food food;

static void reset_game()
{
    printf("! reset game\n");
    image = cv::Scalar(0);
    direction = NONE;

    cv::Size size;
    size.width = width;
    size.height = height;

    image = cv::Mat::zeros(size, CV_8UC3);
    center = Point({width / 2, height / 2});
    snake = Snake(center, cv::Scalar(255, 255, 255));

    for (int i = 1; i <= 3; i++)
    {
        Point pt;
        pt.y = center.y;
        pt.x = center.x - block.width * i;
        snake.points.push_back(pt);
    }

    food.color = cv::Scalar(255, 0, 0);
    generate_food();
    food.exist = true;
}

Snake::Snake(const Point& init_point, cv::Scalar _color)
{
    points.push_back(init_point);
    color = _color;
}

void draw_block(cv::Mat& image, int block_x_idx, int block_y_idx, int block_width, int block_height, cv::Scalar color)
{
    for (int i = 1; i < block_height-1; i++)
    {
        int y = block_y_idx * block_height + i;
        for (int j = 1; j < block_width-1; j++)
        {
            int x = block_x_idx * block_width + j;
            image.ptr(y, x)[0] = color[0];
            image.ptr(y, x)[1] = color[1];
            image.ptr(y, x)[2] = color[2];
        }
    }
}

int get_random_int(int a, int b)
{
    return (rand() * 1.0 / RAND_MAX) * (b - a) + a;
}

bool is_same_block(const Point& p1, const Point& p2, const Block& block)
{
    int p1_block_idx_x = p1.x / block.width;
    int p1_block_idx_y = p1.y / block.height;

    int p2_block_idx_x = p2.x / block.width;
    int p2_block_idx_y = p2.y / block.height;

    return p1_block_idx_x == p2_block_idx_x && p1_block_idx_y == p2_block_idx_y;
}

static void update_snake_body_position()
{
    if (direction != NONE)
    {
        int num_bodies = snake.points.size();
        //printf("num bodies is %d\n", num_bodies);
        for (int i = num_bodies - 1; i > 0; i--)
        {
            snake.points[i].x = snake.points[i - 1].x;
            snake.points[i].y = snake.points[i - 1].y;
        }
    }
}

static void update_snake_head_position(Point& next_pos, bool valid)
{
    if (valid)
    {
        snake.points[0] = next_pos;
    }
}

static Point get_next_position()
{
    Point& point = snake.points[0];
    Point next_pos = point;
    if (direction == RIGHT)
    {
        next_pos.x = point.x + block.width;
    }
    else if (direction == LEFT)
    {
        next_pos.x = point.x - block.width;
    }
    else if (direction == TOP)
    {
        next_pos.y = point.y - block.height;
    }
    else if (direction == BOTTOM)
    {
        next_pos.y = point.y + block.height;
    }
    return next_pos;
}

static void generate_food()
{
    // 随机生成食物的绘制
    Point& pos = food.pos;
    if (!food.exist)
    {
        pos.x = get_random_int(0, width);
        pos.y = get_random_int(0, height);
        food.exist = true;
    }
}

static void eat_food()
{
    int num_bodies = snake.points.size();
    if (is_same_block(snake.points[0], food.pos, block))
    {
        food.exist = false;
        Point new_point;
        Point& last_point = snake.points[num_bodies - 1];

        int x_shift = 0;
        int y_shift = 0;
        if (direction == RIGHT)
        {
            x_shift = -1;
        }
        else if (direction == LEFT)
        {
            x_shift = 1;
        }
        else if (direction == TOP)
        {
            y_shift = 1;
        }
        else if (direction == BOTTOM)
        {
            y_shift = -1;
        }
        new_point.x = (last_point.x + x_shift * block.width) % width;
        new_point.y = (last_point.y + y_shift * block.height) % height;

        snake.points.push_back(new_point);
    }
}

static bool validate_next_pos(Point& next_pos)
{
    // 检查新的 snake 头, 是否撞到墙壁
    const int BLOCKS_IN_COLUMN = width / block.width;
    const int BLOCKS_IN_ROW = height / block.height;
    if (next_pos.x < 0 || next_pos.x == BLOCKS_IN_ROW || next_pos.y < 0 || next_pos.y == BLOCKS_IN_COLUMN)
    {
        reset_game();
        return false;
    }

    // 检查新的 snake 头，是否撞到自己
    for (int i = 1; i < snake.points.size(); i++)
    {
        if (next_pos.x == snake.points[i].x && next_pos.y == snake.points[i].y)
        {
            reset_game();
            return false;
        }
    }
    return true;
}

void update()
{
    // TODO: 检查 block 大小是否符合窗口整除倍数关系
    Point next_pos = get_next_position();
    //printf("netx_pos is: x=%d, y=%d\n", next_pos.x, next_pos.y);
    bool state = validate_next_pos(next_pos);
    update_snake_body_position();
    update_snake_head_position(next_pos, state);
    eat_food();
    generate_food();
}

static void draw_snake()
{
    int num_bodies = snake.points.size();
    for (int i = 0; i < num_bodies; i++)
    {
        Point& point = snake.points[i];

        int block_x_idx = point.x / block.width;
        int block_y_idx = point.y / block.height;
        draw_block(image, block_x_idx, block_y_idx, block.width, block.height, snake.color);
    }
}

static void draw_food()
{
    if (food.exist)
    {
        int block_x_idx = food.pos.x / block.width;
        int block_y_idx = food.pos.y / block.height;
        draw_block(image, block_x_idx, block_y_idx, block.width, block.height, food.color);
    }
}

static void update_direction_from_keyboard()
{
    int key_ret = cv::waitKey(500); // 官方文档说用 waitKeyEx； 下面的数值是我在 Linux KDE 下试出来的。
    switch (key_ret)
    {
    case KEY_LEFT:
        direction = LEFT;
        break;
    case KEY_RIGHT:
        direction = RIGHT;
        break;
    case KEY_TOP:
        direction = TOP;
        break;
    case KEY_BOTTOM:
        direction = BOTTOM;
        break;
    case KEY_EXIT:
        direction = EXIT;
        break;
    default:{}
        //direction = NONE;
    }
    if (key_ret != -1)
    {
        printf("key_ret is %d\n", key_ret);
    }
}

static bool window_should_close()
{
    if (direction == EXIT)
    {
        printf("Bye~\n");
        return true;
    }
    return false;
}

int main()
{
    reset_game();

    int i = 0;
    const char* title = "snake";
    cv::namedWindow(title);
    cv::imshow(title, image);
    while (!window_should_close())
    {
        update_direction_from_keyboard();
        update();
        // i++;
        // std::string image_path = std::to_string(i) + ".png";
        // cv::imwrite(image_path, image);

        image = cv::Scalar(0); // clean canvas
        draw_snake();
        draw_food();
        cv::imshow(title, image);
    }

    return 0;
}